/**
 ** This file is part of FK Klistret Extensions. FK Klistret Extensions 
 ** is free software: you can redistribute it and/or modify it under the 
 ** terms of the GNU General Public License as published by the Free 
 ** Software Foundation, either version 3 of the License, or (at your option) 
 ** any later version.

 ** FK Klistret Extensions is distributed in the hope that it will be 
 ** useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
 ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 ** General Public License for more details. You should have received a
 ** copy of the GNU General Public License along with FK Klistret Extensions. 
 ** If not, see <http://www.gnu.org/licenses/>
 */
package se.fk.klistret.console;

import java.net.URL;
import java.net.MalformedURLException;
import java.io.OutputStream;
import java.io.File;
import java.io.PrintStream;
import java.io.IOException;
import java.io.FileOutputStream;
import java.io.BufferedReader;
import java.io.PrintWriter;
import java.io.FileWriter;
import java.io.FileNotFoundException;
import java.io.InputStreamReader;
import java.io.FileInputStream;
import java.util.List;
import java.util.Properties;
import java.util.LinkedList;
import java.util.Enumeration;
import java.util.Hashtable;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.Target;
import org.apache.tools.ant.DefaultLogger;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.helper.ProjectHelper2;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.Option;
import org.apache.commons.cli.OptionBuilder;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.PosixParser;
import org.apache.commons.cli.HelpFormatter;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.ParseException;

/**
 * 2011-07-11, The comments below are from 2005 and the code is has since
 * upgraded to the Ant 1.8 platform. The patterns and design of this project
 * should not be reused since they are not considered of any quality. The code
 * was written quickly without long term concerns.
 * <p>
 * 
 * 2005, ANTFacade follows the general facade design pattern to simplify the Ant
 * 1.6 system calls. Basically the interface reduces Ant calls to parsing an Ant
 * XML document, executing targets, setting properties, and directing output to
 * log files. Everything else is handled directly by the Ant XML build file
 * regarding the installation process for RFV product (subdomain) deliveries.
 * <p>
 * 
 * Ant is the motor for the installation process. The Ant platform allows
 * application logic to be written in XML and realised through Java code. That
 * is the primary motivation for implementing configuration steps in Ant. The
 * main idea is to verbalise how an installation for specific packages (tar
 * files) should be done in human terms (dvs XML is readable, editable logic)
 * and execute installations through platform independent code (Java). Ant is
 * the glue between installation rules and installation realization.
 * <p>
 * 
 * An instanse of the ANTFacade should (presently) be made for every package
 * installation. The reason being is that Ant properties are static once
 * initialized (unlike program variables). Clients must instantiate an ANTFacade
 * for each installation of a package for a particular product (subdomain). Only
 * then can properties be set prior to parsing the XML build document. Once
 * parsed then logs may be set and targets executed.
 * <p>
 * 
 * Logging of Ant actions is reduced to three types: </br> Message level (piped
 * to standard output), Debug level (piped to output streams/files), and Custom
 * Ant BuildListener (intended to saveinstall information)
 * <p>
 * 
 * Note: Custom BuildListener code expects start/finish events to be fired (in
 * general). The ANTLogListener will not log unless the start method is called
 * and only prints information when the finish events is fired.
 * <p>
 * 
 * Note:The ANTFacade has a user interface access through the main method to
 * test the execution of Ant build files. </br>
 * <code>java se.sfa.rfv.sat.installation.ANTFacade -f build.xml -t [targets]</code>
 * 
 * 
 * @author Matthew Young
 */
public class ANTFacade {

	/**
	 * ProjectHelper2 object used to parse Ant XML documents. Initialized by
	 * constructor.
	 */
	ProjectHelper2 builder;

	/**
	 * Ant Project object acting as a Java interface to Ant XML documents to
	 * execute targets and get/set properties. Initialized by constructor.
	 */
	Project project;

	/**
	 * Extracts all defined properties to the product object.
	 * 
	 * @return List of string objects representing the properties names in the
	 *         Ant XML document.
	 * 
	 */
	@SuppressWarnings("unchecked")
	public List<String> getProperties() {
		List<String> results = new LinkedList<String>();

		Hashtable<String, String> properties = project.getProperties();

		for (Enumeration<?> keys = properties.keys(); keys.hasMoreElements();) {
			String property = (String) keys.nextElement();
			results.add(property);
		}

		return results;
	}

	/**
	 * Return default target
	 * 
	 * @return String
	 */
	public String getDefaultTarget() {
		return project.getDefaultTarget();
	}

	/**
	 * Extracts all target names.
	 * <p>
	 * 
	 * Note: Physical order of targets is not maintained in the generated list
	 * as seen in the Ant XML build file.
	 * 
	 * @return List of string objects representing the target names in the Ant
	 *         XML document.
	 * 
	 */
	@SuppressWarnings("unchecked")
	public List<String> getTargets() {
		List<String> results = new LinkedList<String>();

		Hashtable<String, Target> table = project.getTargets();

		for (Enumeration<?> element = table.elements(); element
				.hasMoreElements();) {
			Target target = (Target) element.nextElement();
			results.add(target.getName());
		}

		return results;
	}

	/**
	 * Gets a property defined either from the Ant XML document or from the Ant
	 * Project interface.
	 * 
	 * @return String
	 * 
	 */
	public String getProperty(String name) {
		return project.getProperty(name);
	}

	/**
	 * Sets a property through the Ant Project interface
	 * <p>
	 * <b>Note: </b> All properties ought to be set prior to building the Ant
	 * XML document.
	 * 
	 * @param name
	 *            <code>String</code>
	 * @param value
	 *            <code>String</code>
	 * 
	 */
	public void setProperty(String name, String value) {
		project.setProperty(name, value);
	}

	/**
	 * Sets an user property through the Ant Project interface
	 * <p>
	 * Note: All properties ought to be set prior to building the Ant XML
	 * document.
	 * 
	 */
	public void setUserProperty(String name, String value) {
		project.setUserProperty(name, value);
	}

	/**
	 * Copies all properties via the Ant Project interface
	 * <p>
	 * Note: All properties ought to be set prior to building the Ant XML
	 * document.
	 * 
	 */
	public void setProperties(Properties properties) {

		if (properties != null) {

			for (Enumeration<?> keys = properties.propertyNames(); keys
					.hasMoreElements();) {
				String key = (String) keys.nextElement();
				setProperty(key, properties.getProperty(key));
			}

		}

	}

	/**
	 * Sets a Ant Project log (standard output and error messages directed to
	 * same output stream). Ant uses LOG4J log levels: DEBUG, INFO, WARN, ERROR
	 * and FATAL.
	 * 
	 */
	private void setLog(OutputStream output, int level) {
		DefaultLogger logger = new DefaultLogger();

		logger.setMessageOutputLevel(level);

		logger.setOutputPrintStream(new PrintStream(output));
		logger.setErrorPrintStream(new PrintStream(output));
		logger.setEmacsMode(true);

		project.addBuildListener(logger);
	}

	/**
	 * Sets output stream for general messages logged by the Ant XML document
	 * (echos and log calls). The messaging level is MSG_INFO.
	 * 
	 */
	public void setMessageLog(OutputStream output) {
		setLog(output, Project.MSG_INFO);
	}

	/**
	 * Sets output stream for debugging messages logged by the Ant XML document
	 * (echos and log calls). The messaging level is MSG_VERBOSE.
	 * 
	 */
	public void setDebugLog(OutputStream output) {
		setLog(output, Project.MSG_VERBOSE);
	}

	/**
	 * Sets output stream for debugging messages logged by the Ant XML document
	 * (echos and log calls). The messaging level is MSG_VERBOSE.
	 * 
	 * @throws SecurityException
	 *             Unable to create file due to permission restrictions.
	 * 
	 * @throws RuntimeException
	 *             Unchecked throw of IOException or FileNotFoundException.
	 * 
	 */
	public void setDebugLog(File file) {

		try {
			file.createNewFile();
		} catch (SecurityException e) {
			throw new SecurityException(String.format(
					"Security error creating debug log file %s: %s",
					file.getName(), e.getMessage()));
		} catch (IOException e) {
			throw new RuntimeException(String.format(
					"IO error creating debug log file %s: %s", file.getName(),
					e.getMessage()));
		}

		try {
			setDebugLog(new FileOutputStream(file, true));
		} catch (FileNotFoundException e) {
			throw new RuntimeException(String.format(
					"File not found error creating debug log file %s: %s",
					file.getName(), e.getMessage()));
		}

	}

	/**
	 * Executes targets defined in an Ant XML document.
	 * 
	 * @throws RuntimeException
	 *             Unchecked throw of an ANT Build exception.
	 * 
	 */
	public void execute(String target) {

		try {
			project.executeTarget(target);
		} catch (BuildException e) {
			throw new RuntimeException(String.format(
					"Failed executing target (%s).  Error: %s", target,
					e.getMessage()));
		}

	}

	/**
	 * Executes default target in an Ant XML document.
	 * 
	 * @throws RuntimeException
	 *             Unchecked throw of an ANT Build exception.
	 * 
	 */
	public void execute() {
		try {
			project.executeTarget(project.getDefaultTarget());
		} catch (BuildException e) {
			throw new RuntimeException(String.format(
					"Failed executing default target (%s). Error: %s",
					project.getDefaultTarget() + "). Error: ", e.getMessage()));
		}

	}

	/**
	 * Notifies the Ant Project interface that the build (usage) of the Ant XML
	 * document is finished (dvs all necessary targets have been executed)
	 * <p>
	 * Note: Package installation routines must call this method to complete XML
	 * logging (otherwise the package is never logged as installed).
	 * 
	 */
	public void finished(Throwable exception) {
		project.fireBuildFinished(exception);
	}

	/**
	 * Notifies the Ant Project interface that the build (usage) of the Ant XML
	 * document has started.
	 * <p>
	 * Note: Package installation routines must call this method to initialize
	 * XML logging (otherwise the package is never logged as installed).
	 * 
	 */
	public void start() {
		project.fireBuildStarted();
	}

	/**
	 * Reads URL data stream into a temporary file used by the ANT parser since
	 * the parse method only supports File objects.
	 * <p>
	 * Note: Future releases of Ant may support URL references via the parse
	 * method. This method is unwanted since data must be streamed twice (first
	 * from an URL and then again from a File).
	 * 
	 * @throws RuntimeException
	 *             Unchecked throw of IOException
	 * 
	 */
	private File transformURL(URL url) {

		try {
			File file = File.createTempFile("save_url_input", null);
			file.deleteOnExit();

			BufferedReader reader = new BufferedReader(new InputStreamReader(
					url.openStream()));

			FileWriter filewriter = new java.io.FileWriter(file);
			PrintWriter printwriter = new java.io.PrintWriter(filewriter, true);

			String inputLine;
			while ((inputLine = reader.readLine()) != null) {
				printwriter.println(inputLine);
			}

			reader.close();
			printwriter.close();

			return file;
		} catch (IOException e) {
			throw new RuntimeException(
					String.format(
							"Failed to transform URL into File (%s). The current Ant version does not support direct parsing of URLs.  Error: %s",
							url.getPath(), e.getMessage()));
		}

	}

	/**
	 * Parses the Ant XML document which allows targets to be executed.
	 * <p>
	 * Note: ANTFacade constructor initializes the Ant Project interface and
	 * calls the <b><font size="-1"> init</font></b> method.
	 * 
	 * @throws RuntimeException
	 *             Unchecked throw of an ANT Build exception.
	 */
	public void parse(URL url) {

		try {
			builder.parse(project, transformURL(url));
		} catch (BuildException e) {
			throw new RuntimeException(
					String.format(
							"Parsing the ANT build document failed (url parsed: %s). Error: %s",
							url.getPath(), e.getMessage()));
		}

	}

	/**
	 * Parses the Ant XML document which allows targets to be executed.
	 * <p>
	 * Note: ANTFacade constructor initializes the Ant Project interface and
	 * calls the <b><font size="-1"> init</font></b> method.
	 * 
	 * @throws RuntimeException
	 *             Unchecked throw of an ANT Build exception.
	 * 
	 */
	public void parse(File file) {

		try {
			builder.parse(project, file);
		} catch (BuildException e) {
			throw new RuntimeException(
					String.format(
							"Parsing the ANT build document failed (file parsed: %s). Error: %s",
							file.getPath(), e.getMessage()));
		}

	}

	/**
	 * Constructor initializes a ProjectHelper2 and a Project objects then calls
	 * the init method.
	 * 
	 * @throws RuntimeException
	 *             Unchecked throw of an ANT Build exception if the Ant project
	 *             object can not be initialized.
	 * 
	 */
	public ANTFacade() {
		try {
			builder = new ProjectHelper2();
			project = new Project();

			project.init();
		} catch (BuildException e) {
			throw new RuntimeException(
					String.format(
							"Error initializing Ant project object and default logger: %s",
							e.getMessage()));
		}
	}

	/**
	 * printHelp
	 */
	private static void printHelp() {
		System.out.println("-- Help --" + "\n"
				+ "ANTFacade is a facade to the Ant Java interface to provide "
				+ "\n"
				+ "basic execution and property access functionality.  A file "
				+ "\n"
				+ "or URL document may be passed (dvs -f, -u options) to the "
				+ "\n"
				+ "ANTFacade parser and a list of targets to be executed ("
				+ "\n" + "otherwise the default target is used).");
	}

	/**
	 * User interface only.
	 * 
	 */
	@SuppressWarnings("static-access")
	public static void main(String[] args) throws Exception {
		Options options = new Options();

		Option usage = OptionBuilder.withLongOpt("usage")
				.withDescription("ANTFacade usage").create();

		Option help = OptionBuilder.withLongOpt("help")
				.withDescription("ANTFacade help").create();

		Option debug = new Option("d", "Debug");

		Option buildurl = OptionBuilder.withArgName("url").hasArg()
				.withDescription("URL to Ant/XML build document")
				.withLongOpt("url").create("u");

		Option buildfile = OptionBuilder.withArgName("file path").hasArg()
				.withDescription("File path to Ant/XML build document")
				.withLongOpt("file").create("f");

		Option targets = OptionBuilder.withArgName("target list [target]+")
				.hasArgs().withDescription("Targets to execute")
				.withLongOpt("targets").create("t");

		Option properties = OptionBuilder.withArgName("property file")
				.hasArgs().withDescription("Load all properties from file")
				.withLongOpt("propertyfile").create("p");

		Option single_property = OptionBuilder
				.withArgName("property list [property=value]+").hasArgs()
				.withDescription("use value for given property").create("D");

		Option static_single_property = OptionBuilder
				.withArgName("property list [property=value]+").hasArgs()
				.withDescription("use value for given STATIC property")
				.create("S");

		options.addOption(usage);
		options.addOption(help);
		options.addOption(debug);
		options.addOption(buildfile);
		options.addOption(buildurl);
		options.addOption(targets);
		options.addOption(properties);
		options.addOption(single_property);
		options.addOption(static_single_property);

		CommandLineParser parser = new PosixParser();
		HelpFormatter formatter = new HelpFormatter();

		try {
			CommandLine line = parser.parse(options, args);

			ANTFacade ant = new ANTFacade();

			if (line.hasOption("usage")) {
				formatter.printHelp("ANTFacade", options);
				System.exit(0);
			}

			if (line.hasOption("help")) {
				ANTFacade.printHelp();
				System.exit(0);
			}

			if (line.hasOption("d")) {
				ant.setLog(System.out, Project.MSG_DEBUG);
			} else {
				ant.setLog(System.out, Project.MSG_INFO);
			}

			if (line.hasOption("p")) {
				Properties file_properties = new Properties();

				file_properties.load(new FileInputStream(new File(line
						.getOptionValue("p"))));
				ant.setProperties(file_properties);
			}

			if (line.hasOption("D")) {
				String[] property_array = line.getOptionValues("D");

				for (int index = 0; index < property_array.length; index++) {
					String value = property_array[index];

					if (value.indexOf("=") == -1) {
						System.err
								.println("Single properties must be delimited with an equal sign"
										+ "\t(" + value + ").");
						formatter.printHelp("ANTFacade", options);
						System.exit(2);
					}

					String property_name = value.substring(0,
							value.indexOf("="));
					String property_value = value.substring(
							value.indexOf("=") + 1, value.length());

					ant.setProperty(property_name, property_value);
				}
			}

			if (line.hasOption("S")) {
				String[] property_array = line.getOptionValues("S");

				for (int index = 0; index < property_array.length; index++) {
					String value = property_array[index];

					if (value.indexOf("=") == -1) {
						System.err
								.println("Static single properties must be delimited with an equal sign"
										+ "\t(" + value + ").");
						formatter.printHelp("ANTFacade", options);
						System.exit(2);
					}

					String property_name = value.substring(0,
							value.indexOf("="));
					String property_value = value.substring(
							value.indexOf("=") + 1, value.length());

					ant.setUserProperty(property_name, property_value);
				}
			}

			if (line.hasOption("f")) {
				ant.parse(new File(line.getOptionValue("f")));
			}

			if (line.hasOption("u")) {
				ant.parse(new URL(line.getOptionValue("u")));
			}

			if (line.hasOption("t")) {
				String[] target_array = line.getOptionValues("t");

				for (int index = 0; index < target_array.length; index++) {
					ant.execute(target_array[index]);
				}
			} else {
				if (ant.getDefaultTarget() == null) {
					System.err.println("Ant build file has no default target.");
					formatter.printHelp("ANTFacade", options);
					System.exit(2);
				} else {
					ant.execute();
				}
			}
		} catch (ParseException e) {
			System.err.println(String.format(
					"Parsing command line failed.  Error: %s", e.getMessage()));
			formatter.printHelp("ANTFacade", options);
		} catch (MalformedURLException e) {
			System.err.println(String.format(
					"URL malformed (http or file protocol).  Error: %s",
					e.getMessage()));
		} catch (NullPointerException e) {
			System.err
					.println(String
							.format("Nullpointer exception (likely with -f,--file input).  Error: %s",
									e.getMessage()));
		}
	}
}
